package com.demo.springbootdemonettywebsocket.socket;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.demo.springbootdemonettywebsocket.pojo.MessagePojo;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.MultiValueMap;
import org.yeauty.annotation.*;
import org.yeauty.pojo.Session;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * bossLoopGroupThreads建议设置1
 * workerLoopGroupThreads建议设置为CPU*2
 * 另外需要查看服务器的文件打开数是否有限制
 *
 * @author peter
 * @date 2022/5/18 上午8:37
 */
@ServerEndpoint(path = "/ws/json", port = "${ws.port}", maxFramePayloadLength = "131070", childOptionWriteBufferHighWaterMark = "131070",readerIdleTimeSeconds = "0")
@Slf4j
public class JsonWebSocket {

    //静态变量，用来记录当前在线连接数。应该把它设计成线程安全的。
    private static AtomicInteger onlineCount = new AtomicInteger(0);

    // 一个用户允许打开一个连接
    private static ConcurrentHashMap<String, Map<String, JsonWebSocket>> webSocketClientMaps = new ConcurrentHashMap<String, Map<String, JsonWebSocket>>();

    private Session session;
    private String username;

    @BeforeHandshake
    public void handshake(Session session, HttpHeaders headers, @RequestParam String req, @RequestParam MultiValueMap reqMap, @PathVariable String arg, @PathVariable Map pathMap){
        session.setSubprotocols("stomp");

        // 在线网站测试
        // http://www.websocket-test.com/
        // ws:127.0.0.1:18009/ws/json?req=ok&name=peter
        // ws:127.0.0.1:18009/ws/json?req=ok&name=andy

        // {"route":"/message","message":"你好"}
        log.info("req->" + req);// req->ok
        log.info("reqMap->" + reqMap.toString());// reqMap->{req=[ok], name=[peter]}
        log.info("arg->" + arg);// arg->hello
        log.info("pathMap->" + pathMap.toString());// pathMap->{arg=hello}

        if (!"ok".equals(req)){
            log.warn("Authentication failed!");
            session.close();
        }
    }

    /**
     * 当有新的WebSocket连接进入时，对该方法进行回调 注入参数的类型:Session、HttpHeaders、ParameterMap
     * @param session
     * @param headers
     * @param username
     * @param reqMap
     * @param arg
     * @param pathMap
     */
    @OnOpen
    public void onOpen(Session session, HttpHeaders headers, @RequestParam String username, @RequestParam MultiValueMap reqMap, @PathVariable String arg, @PathVariable Map pathMap) {
        int i = onlineCount.incrementAndGet();
        log.info("onOpen {} -> {}, session.id[{}]上线, 在线连接数[{}]",
                session.channel().remoteAddress().toString(),
                session.channel().localAddress().toString(),
                session.id(),
                String.valueOf(i)
        );
        if (session.isOpen()) {

            this.session = session;

            // ArrayList name = (ArrayList) reqMap.getOrDefault("name", new String[]{"游客"});
            session.sendText("欢迎 : "+ username);

            // 方法一,记录用户名: 写入到成员变量, 然后记录到Map中
            this.username = username;
            setWebSocketClientMaps(session);

            // https://github.com/YeautyYE/netty-websocket-spring-boot-starter/issues/19
            // 方法二,记录用户名: 绑定channel自定义属性, 这里可以自定义数据
            AttributeKey<String> sessionIdKey = AttributeKey.valueOf("username");
            session.channel().attr(sessionIdKey).set(this.username);
            String s = session.channel().attr(sessionIdKey).get();

            // 方法三
            session.setAttribute("username", this.username);
            log.info("channel attr username->" + s);
        }
    }


    /**
     * 当有WebSocket连接关闭时，对该方法进行回调 注入参数的类型:Session
     *
     * @param session
     * @throws IOException
     */
    @OnClose
    public void onClose(Session session) throws IOException {
        int i = onlineCount.decrementAndGet();
        log.info("OnClose -> [" + session.id() + "]下线, 在线连接数" +String.valueOf(i));
    }

    /**
     * 当有WebSocket抛出异常时，对该方法进行回调 注入参数的类型:Session、Throwable
     * @param session
     * @param throwable
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        throwable.printStackTrace();
    }

    /**
     * 当接收到字符串消息时，对该方法进行回调 注入参数的类型:Session、String
     * @param session
     * @param message
     */
    @OnMessage
    public void onMessage(Session session, String message) {
        AttributeKey<String> sessionIdKey = AttributeKey.valueOf("username");
        session.channel().attr(sessionIdKey).set(this.username);
        String s = session.channel().attr(sessionIdKey).get();

        log.info("JsonWebSocket user->{}", this.username);
        log.info("channel attr->" + s);
        log.info("session attr->" + session.getAttribute("username").toString());

        if ("ping".equals(message.toLowerCase().toString())) {

            session.sendText("pong");

        } else {

            MessagePojo messagePojo = JSONUtil.toBean(message, MessagePojo.class);
            log.info("route->" + messagePojo.getRoute());
            log.info("message->" + messagePojo.getMessage());

            // session.sendText("Hello Netty!");
            for (Map.Entry<String, Map<String, JsonWebSocket>> wsClient: webSocketClientMaps.entrySet()) {
                String usernameStr = wsClient.getKey();
                if (usernameStr != this.username) {
                    Session toUserSession = wsClient.getValue().get(usernameStr).session;
                    toUserSession.sendText(StrUtil.format("来自[{}]消息: {}", username, message));
                }
            }

        }

    }

    @OnBinary
    public void onBinary(Session session, byte[] bytes) {
        for (byte b : bytes) {
            System.out.println(b);
        }
        session.sendBinary(bytes);
    }

    /**
     * 当接收到Netty的事件时，对该方法进行回调 注入参数的类型:Session、Object
     * @param session
     * @param evt
     */
    @OnEvent
    public void onEvent(Session session, Object evt) {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
            switch (idleStateEvent.state()) {
                case READER_IDLE:
                    System.out.println("read idle");
                    if (session.isOpen()) {
                        session.sendText("PING");
                    }
                    break;
                case WRITER_IDLE:
                    System.out.println("write idle");
                    if (session.isOpen()) {
                        session.sendText("PING");
                    }
                    break;
                case ALL_IDLE:
                    System.out.println("all idle");
                    if (session.isOpen()) {
                        session.sendText("PING");
                    }
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * 根据Session获取客户端表映射
     *
     * @param username
     * @return
     */
    private Session getSessionByUsername(String username) {

        for (Map.Entry<String, Map<String, JsonWebSocket>> wsClient: webSocketClientMaps.entrySet()) {
            String usernameStr = wsClient.getKey();
            if (usernameStr == username) {
                return wsClient.getValue().get(this.username).session;
            }
        }

        return null;
    }

    /**
     * 设置用户和会话关系
     * @param session 连接信息
     */
    private void setWebSocketClientMaps(Session session) {

        if (webSocketClientMaps.containsKey(this.username)) {
            Map<String, JsonWebSocket> oldClient = webSocketClientMaps.get(this.username);
            log.warn("用户已存在{}", this.username);

            // 用户已连接,下线旧的连接
            try {
                onClose(oldClient.get(this.username).session);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        Map<String, JsonWebSocket> jsonWebSocketSessionMap = new HashMap<>();
        jsonWebSocketSessionMap.put(this.username, this);
        webSocketClientMaps.putIfAbsent(this.username, jsonWebSocketSessionMap);
    }
}
